03 - 环境接口标准
前置:01 篇 里那张 rollout 链路图。
本篇回答:训练框架和环境之间该用什么接口对话?为什么这件事到 2026 年还没统一,三套主流方案各自把线划在了哪里?
本篇会用到的词:
| 词 | 意思 |
|---|---|
| Gymnasium | 经典强化学习的环境接口库,OpenAI Gym 的继任者。它定义的 reset() / step(action) 是整个 RL 领域的通用心智模型 |
| observation | 环境返回给模型的东西。对 Agent 来说通常是命令的标准输出、文件内容、网页快照 |
| harness(脚手架) | 真正驱动模型干活的那个程序,比如 Claude Code、Codex、mini-swe-agent。它管提示词、工具调用、循环控制 |
| taskset | 一组任务的集合与加载器,每条任务带着自己的初始文件、参考答案和评分方式 |
| trace | 一次运行的完整记录:消息图、奖励、指标、每次模型调用的用量与耗时 |
| rubric | 打分规则。可以是「测试过没过」这种硬判定,也可以是让另一个模型来评 |
| MCP | Model Context Protocol,规定 Agent 怎么发现和调用外部工具的协议,见 网关 05 篇 |
一、同一条 rollout,三个不同的切分位置
02 篇解决的是「环境怎么高效地跑起来」。这一篇的问题不同:训练框架和环境之间,那条接口该画在哪儿。
三套主流方案给出了三个不同的答案,而且它们不是竞争同一个位置,是划在了同一条 rollout 的不同地方:
二、OpenEnv:把经典 RL 的接口搬到 HTTP 上
huggingface/OpenEnv(★2,509,BSD-3-Clause,2025-10-01 建仓)。它的定位写得很直接:用 Gymnasium 风格的简单 API 做 agentic RL 训练的执行环 境。
核心只有三个方法:reset() / step() / state()。
import asyncio
from echo_env import CallToolAction, EchoEnv
async def main():
# 环境是一个 HTTP 服务,客户端通过 base_url 连过去
# 这一点很关键:环境和训练进程可以完全不在一台机器上
async with EchoEnv(base_url="https://openenv-echo-env.hf.space") as client:
# reset:把环境恢复到初始状态,返回第一个观察
# 对应 01 篇讲的「每条 rollout 从同一起点开始」
result = await client.reset()
print(result.observation.echoed_message)
# step:交一个动作进去,拿回新的观察和这一步的奖励
# 动作是结构化的,不是一段裸字符串 —— 这里是「调用某个工具」
result = await client.step(
CallToolAction(
tool_name="echo_message",
arguments={"message": "Hello, World!"},
)
)
print(result.observation.result)
print(result.reward) # 这一步拿到的奖励
asyncio.run(main())
三个设计选择值得注意:
① 环境是一个 HTTP 服务,用 Docker 打包。 这意味着环境作者和训练框架作者可以完全解耦:前者交付一个镜像,后者只认接口。OpenEnv 的 CLI 还能把环境直接部署到 Hugging Face Spaces。
② 动作是结构化的。 上面例子里传的是 CallToolAction(tool_name=..., arguments=...),不是一段自由文本。这让环境侧可以做严格校验,也让「模型输出了不合法的动作」成为一个可以明确处理的情况。
③ 官方明确标注为实验阶段。 README 里有 Early Development Warning,说明会有破坏性变更 —— 和 可观测性 02 篇里 OTel GenAI 的处境类似,这个领域的接口标准都还没稳。
它的边界:step() 这个抽象假设了「一步」是清晰可分的。但真实的编码 Agent 里,一次「步」可能是模型自己在内部循环调了五个工具 —— 要么把粒度拆得很细导致 HTTP 往返次数爆炸,要么粒度粗到 step 语义变得模糊。这正是下一节那套方案想绕开的问题。
三、verifiers:把一整条轨迹当作接口单位
PrimeIntellect-ai/verifiers(★4,525,MIT,2025-01-22 建仓),Prime Intellect 出品,和它家的训练框架 prime-rl 与 Environments Hub 深度绑定。
v1 版本的对象模型和 Gymnasium 那一套完全不同:
| 概念 | 含义 |
|---|---|
| taskset | 任务集合与加载器。每条任务带着自己的提示词、初始文件、参考答案和资源需求 |
| harness | 模型实际被放进去跑的那个程序 —— 官方举的例子就是 Claude Code、Codex、mini-swe-agent |
| agent | harness × 模型 × 运行时策略的组合,它产出一条 trace |
| environment | 含一个或多个 agent,定义它们之间的控制流 |
| toolset | 任务定义的一组工具,以 MCP Server 的形式装进支持它的 harness |
| trace | 消息图、奖励、指标、错误,以及每次模型调用的模型名、采样参数、结束原因、用量、耗时 |
这套模型的好处:真实 harness 可以原样拿来训练。你训的就是「Claude Code 加上你的模型」这个整体,而不是一个为了训练特意简化过的复制品——训练与部署之间的分布差异因此小很多。
代价:训练框架失去 了对中间每一步的细粒度控制。要做逐步奖励、要在第 7 步截断、要对某一步单独重采样,都变难了。
四、agent-lightning:不定义环境接口,在模型 API 层拦
microsoft/agent-lightning(★17,525,MIT,2025-06-18 建仓)走的是第三条路,而且它是这三个里 star 最高的。
v1.0 的整个框架只有约 3,500 行代码,官方把简洁列为第一原则。它的关键主张是:agent 代码零改动。

图片来自 microsoft/agent-lightning 官方仓库 docs/images/architecture.jpg(MIT)
三个组件:
| 组件 | 职责 |
|---|---|
| Trainer | 跑 verl 和 vLLM,构造训练样本,更新策略 |
| API Gateway | 代理模型请求,顺手把交互过程捕获成训练数据 |
| Rollout Controller | 在本地或以 Kubernetes Job 的形式拉起 agent |
要点在中间那个 Gateway:agent 以为自己在调一个普通的模型 API,实际上请求经过了代理,代理一边转发一边把「什么提示词进去、什么输出出来」记下来。于是工具、上下文、控制流、环境全都保持原 样在循环里,不需要为训练重写任何东西。
另一个值得注意的选择是 Rollout Controller 原生用 Kubernetes Job 跑 agent,不依赖外部沙箱服务。这是和 02 篇 AgentENV 完全相反的取舍:AgentENV 认为环境层值得专门做一套基础设施;agent-lightning 认为 K8s 已经够了,把复杂度留在训练侧。
官方给的效果数据(引自其 README 与技术报告):用 6K 训练样本,Qwen3.5-9B 的端到端流程把 SWE-bench Verified 从 41.8% 提到 56.4%,提升 14.6 个百分点。
它的边界:在 API 层拦截意味着你只看得见「模型说了什么」,看不见环境内部状态。如果你的奖励需要检查环境里的文件、进程、数据库,还是得另外接一套判定逻辑。
五、怎么选
| 你的情况 | 建议 |
|---|---|
| 任务本身就是一个清晰的回合制交互(游戏、工具调用序列) | OpenEnv —— step / reset 语义天然匹配,生态里现成环境也最多 |
| 你想直接训练现有的编码 agent,且不打算改它的代码 | agent-lightning —— 侵入最小,K8s 就能跑起来 |
| 你要建立一批可复用、可共享、带标准打分的任务集 | verifiers —— taskset + Environments Hub 就是为这件事做的 |
| 你在做一个训练框架,需要同时支持上面几种 | 先做内部抽象,别直接绑定任何一套 —— 理由和 可观测性 02 篇里那条一样:没有一套是 稳定的 |
最后这条值得强调:三套方案里最老的 verifiers 也才 2025 年 1 月建仓,OpenEnv 更是 2025 年 10 月才有,且自己标着实验阶段。在这个阶段把接口写死进业务代码,等于给自己埋一次重写。
下一篇 → 04 - 训练框架怎么接环境:verl、SkyRL、ROLL、prime-rl 各自怎么把环境接进 rollout 循环,以及「谁负责把环境拉起来」这个分工问题。
← 回到 专题索引 · Agent Infra 板块总览